#    (c) Copyright 2014-2015 Hewlett Packard Enterprise Development LP
#    All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
#
"""HPE LeftHand SAN ISCSI REST Proxy.

Volume driver for HPE LeftHand Storage array.
This driver requires 11.5 or greater firmware on the LeftHand array, using
the 2.0 or greater version of the hpelefthandclient.

You will need to install the python hpelefthandclient module.
sudo pip install python-lefthandclient

Set the following in the cinder.conf file to enable the
LeftHand iSCSI REST Driver along with the required flags:

volume_driver=cinder.volume.drivers.hpe.hpe_lefthand_iscsi.
    HPELeftHandISCSIDriver

It also requires the setting of hpelefthand_api_url, hpelefthand_username,
hpelefthand_password for credentials to talk to the REST service on the
LeftHand array.

"""

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import importutils
from oslo_utils import units

from cinder import context
from cinder import exception
from cinder.i18n import _, _LE, _LI, _LW
from cinder.volume import driver
from cinder.volume import utils
from cinder.volume import volume_types

import six

import math
import re

LOG = logging.getLogger(__name__)

hpelefthandclient = importutils.try_import("hpelefthandclient")
if hpelefthandclient:
    from hpelefthandclient import client as hpe_lh_client
    from hpelefthandclient import exceptions as hpeexceptions

hpelefthand_opts = [
    cfg.StrOpt('hpelefthand_api_url',
               default=None,
               help="HPE LeftHand WSAPI Server Url like "
                    "https://<LeftHand ip>:8081/lhos",
               deprecated_name='hplefthand_api_url'),
    cfg.StrOpt('hpelefthand_username',
               default=None,
               help="HPE LeftHand Super user username",
               deprecated_name='hplefthand_username'),
    cfg.StrOpt('hpelefthand_password',
               default=None,
               help="HPE LeftHand Super user password",
               secret=True,
               deprecated_name='hplefthand_password'),
    cfg.StrOpt('hpelefthand_clustername',
               default=None,
               help="HPE LeftHand cluster name",
               deprecated_name='hplefthand_clustername'),
    cfg.BoolOpt('hpelefthand_iscsi_chap_enabled',
                default=False,
                help='Configure CHAP authentication for iSCSI connections '
                '(Default: Disabled)',
                deprecated_name='hplefthand_iscsi_chap_enabled'),
    cfg.BoolOpt('hpelefthand_debug',
                default=False,
                help="Enable HTTP debugging to LeftHand",
                deprecated_name='hplefthand_debug'),

]

CONF = cfg.CONF
CONF.register_opts(hpelefthand_opts)

MIN_API_VERSION = "1.1"
MIN_CLIENT_VERSION = '2.0.0'

# map the extra spec key to the REST client option key
extra_specs_key_map = {
    'hpelh:provisioning': 'isThinProvisioned',
    'hpelh:ao': 'isAdaptiveOptimizationEnabled',
    'hpelh:data_pl': 'dataProtectionLevel',
    'hplh:provisioning': 'isThinProvisioned',
    'hplh:ao': 'isAdaptiveOptimizationEnabled',
    'hplh:data_pl': 'dataProtectionLevel',
}

# map the extra spec value to the REST client option value
extra_specs_value_map = {
    'isThinProvisioned': {'thin': True, 'full': False},
    'isAdaptiveOptimizationEnabled': {'true': True, 'false': False},
    'dataProtectionLevel': {
        'r-0': 0, 'r-5': 1, 'r-10-2': 2, 'r-10-3': 3, 'r-10-4': 4, 'r-6': 5}
}


class HPELeftHandISCSIDriver(driver.ISCSIDriver):
    """Executes REST commands relating to HPE/LeftHand SAN ISCSI volumes.

    Version history:
        1.0.0 - Initial REST iSCSI proxy
        1.0.1 - Added support for retype
        1.0.2 - Added support for volume migrate
        1.0.3 - Fixed bug #1285829, HP LeftHand backend assisted migration
                should check for snapshots
        1.0.4 - Fixed bug #1285925, LeftHand AO volume create performance
                improvement
        1.0.5 - Fixed bug #1311350, Live-migration of an instance when
                attached to a volume was causing an error.
        1.0.6 - Removing locks bug #1395953
        1.0.7 - Fixed bug #1353137, Server was not removed from the HP
                Lefthand backend after the last volume was detached.
        1.0.8 - Fixed bug #1418201, A cloned volume fails to attach.
        1.0.9 - Adding support for manage/unmanage.
        1.0.10 - Add stats for goodness_function and filter_function
        1.0.11 - Add over subscription support
        1.0.12 - Adds consistency group support
        1.0.13 - Added update_migrated_volume #1493546
        1.0.14 - Removed the old CLIQ based driver
        2.0.0 - Rebranded HP to HPE
        2.0.1 - Remove db access for consistency groups
    """

    VERSION = "2.0.1"

    device_stats = {}

    def __init__(self, *args, **kwargs):
        super(HPELeftHandISCSIDriver, self).__init__(*args, **kwargs)
        self.configuration.append_config_values(hpelefthand_opts)
        if not self.configuration.hpelefthand_api_url:
            raise exception.NotFound(_("HPELeftHand url not found"))

        # blank is the only invalid character for cluster names
        # so we need to use it as a separator
        self.DRIVER_LOCATION = self.__class__.__name__ + ' %(cluster)s %(vip)s'

    def _login(self):
        client = self._create_client()
        try:
            if self.configuration.hpelefthand_debug:
                client.debug_rest(True)

            client.login(
                self.configuration.hpelefthand_username,
                self.configuration.hpelefthand_password)

            cluster_info = client.getClusterByName(
                self.configuration.hpelefthand_clustername)
            self.cluster_id = cluster_info['id']
            virtual_ips = cluster_info['virtualIPAddresses']
            self.cluster_vip = virtual_ips[0]['ipV4Address']

            return client
        except hpeexceptions.HTTPNotFound:
            raise exception.DriverNotInitialized(
                _('LeftHand cluster not found'))
        except Exception as ex:
            raise exception.DriverNotInitialized(ex)

    def _logout(self, client):
        client.logout()

    def _create_client(self):
        return hpe_lh_client.HPELeftHandClient(
            self.configuration.hpelefthand_api_url)

    def do_setup(self, context):
        """Set up LeftHand client."""
        if hpelefthandclient.version < MIN_CLIENT_VERSION:
            ex_msg = (_("Invalid hpelefthandclient version found ("
                        "%(found)s). Version %(minimum)s or greater "
                        "required. Run 'pip install --upgrade "
                        "python-lefthandclient' to upgrade the "
                        "hpelefthandclient.")
                      % {'found': hpelefthandclient.version,
                         'minimum': MIN_CLIENT_VERSION})
            LOG.error(ex_msg)
            raise exception.InvalidInput(reason=ex_msg)

    def check_for_setup_error(self):
        """Checks for incorrect LeftHand API being used on backend."""
        client = self._login()
        try:
            self.api_version = client.getApiVersion()

            LOG.info(_LI("HPELeftHand API version %s"), self.api_version)

            if self.api_version < MIN_API_VERSION:
                LOG.warning(_LW("HPELeftHand API is version %(current)s. "
                                "A minimum version of %(min)s is needed for "
                                "manage/unmanage support."),
                            {'current': self.api_version,
                             'min': MIN_API_VERSION})
        finally:
            self._logout(client)

    def get_version_string(self):
        return (_('REST %(proxy_ver)s hpelefthandclient %(rest_ver)s') % {
            'proxy_ver': self.VERSION,
            'rest_ver': hpelefthandclient.get_version_string()})

    def create_volume(self, volume):
        """Creates a volume."""
        client = self._login()
        try:
            # get the extra specs of interest from this volume's volume type
            volume_extra_specs = self._get_volume_extra_specs(volume)
            extra_specs = self._get_lh_extra_specs(
                volume_extra_specs,
                extra_specs_key_map.keys())

            # map the extra specs key/value pairs to key/value pairs
            # used as optional configuration values by the LeftHand backend
            optional = self._map_extra_specs(extra_specs)

            # if provisioning is not set, default to thin
            if 'isThinProvisioned' not in optional:
                optional['isThinProvisioned'] = True

            # AdaptiveOptimization defaults to 'true' if you don't specify the
            # value on a create, and that is the most efficient way to create
            # a volume. If you pass in 'false' or 'true' for AO, it will result
            # in an update operation following the create operation to set this
            # value, so it is best to not specify the value and let it default
            # to 'true'.
            if optional.get('isAdaptiveOptimizationEnabled'):
                del optional['isAdaptiveOptimizationEnabled']

            clusterName = self.configuration.hpelefthand_clustername
            optional['clusterName'] = clusterName

            volume_info = client.createVolume(
                volume['name'], self.cluster_id,
                volume['size'] * units.Gi,
                optional)

            return self._update_provider(volume_info)
        except Exception as ex:
            raise exception.VolumeBackendAPIException(data=ex)
        finally:
            self._logout(client)

    def delete_volume(self, volume):
        """Deletes a volume."""
        client = self._login()
        try:
            volume_info = client.getVolumeByName(volume['name'])
            client.deleteVolume(volume_info['id'])
        except hpeexceptions.HTTPNotFound:
            LOG.error(_LE("Volume did not exist. It will not be deleted"))
        except Exception as ex:
            raise exception.VolumeBackendAPIException(ex)
        finally:
            self._logout(client)

    def extend_volume(self, volume, new_size):
        """Extend the size of an existing volume."""
        client = self._login()
        try:
            volume_info = client.getVolumeByName(volume['name'])

            # convert GB to bytes
            options = {'size': int(new_size) * units.Gi}
            client.modifyVolume(volume_info['id'], options)
        except Exception as ex:
            raise exception.VolumeBackendAPIException(ex)
        finally:
            self._logout(client)

    def create_consistencygroup(self, context, group):
        """Creates a consistencygroup."""
        model_update = {'status': 'available'}
        return model_update

    def create_consistencygroup_from_src(self, context, group, volumes,
                                         cgsnapshot=None, snapshots=None,
                                         source_cg=None, source_vols=None):
        """Creates a consistency group from a source"""
        msg = _("Creating a consistency group from a source is not "
                "currently supported.")
        LOG.error(msg)
        raise NotImplementedError(msg)

    def delete_consistencygroup(self, context, group, volumes):
        """Deletes a consistency group."""
        volume_model_updates = []
        for volume in volumes:
            volume_update = {'id': volume.id}
            try:
                self.delete_volume(volume)
                volume_update['status'] = 'deleted'
            except Exception as ex:
                LOG.error(_LE("There was an error deleting volume %(id)s: "
                              "%(error)."),
                          {'id': volume.id,
                           'error': six.text_type(ex)})
                volume_update['status'] = 'error'
            volume_model_updates.append(volume_update)

        model_update = {'status': group.status}

        return model_update, volume_model_updates

    def update_consistencygroup(self, context, group,
                                add_volumes=None, remove_volumes=None):
        """Updates a consistency group.

        Because the backend has no concept of volume grouping, cinder will
        maintain all volume/consistency group relationships. Because of this
        functionality, there is no need to make any client calls; instead
        simply returning out of this function allows cinder to properly
        add/remove volumes from the consistency group.
        """
        return None, None, None

    def create_cgsnapshot(self, context, cgsnapshot, snapshots):
        """Creates a consistency group snapshot."""
        client = self._login()
        try:
            snap_set = []
            snapshot_base_name = "snapshot-" + cgsnapshot.id
            snapshot_model_updates = []
            for i, snapshot in enumerate(snapshots):
                volume = snapshot.volume
                volume_name = volume['name']
                try:
                    volume_info = client.getVolumeByName(volume_name)
                except Exception as ex:
                    error = six.text_type(ex)
                    LOG.error(_LE("Could not find volume with name %(name)s. "
                                  "Error: %(error)s"),
                              {'name': volume_name,
                               'error': error})
                    raise exception.VolumeBackendAPIException(data=error)

                volume_id = volume_info['id']
                snapshot_name = snapshot_base_name + "-" + six.text_type(i)
                snap_set_member = {'volumeName': volume_name,
                                   'volumeId': volume_id,
                                   'snapshotName': snapshot_name}
                snap_set.append(snap_set_member)
                snapshot_update = {'id': snapshot['id'],
                                   'status': 'available'}
                snapshot_model_updates.append(snapshot_update)

            source_volume_id = snap_set[0]['volumeId']
            optional = {'inheritAccess': True}
            description = cgsnapshot.description
            if description:
                optional['description'] = description

            try:
                client.createSnapshotSet(source_volume_id, snap_set, optional)
            except Exception as ex:
                error = six.text_type(ex)
                LOG.error(_LE("Could not create snapshot set. Error: '%s'"),
                          error)
                raise exception.VolumeBackendAPIException(
                    data=error)

        except Exception as ex:
            raise exception.VolumeBackendAPIException(data=six.text_type(ex))
        finally:
            self._logout(client)

        model_update = {'status': 'available'}

        return model_update, snapshot_model_updates

    def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
        """Deletes a consistency group snapshot."""

        client = self._login()
        snap_name_base = "snapshot-" + cgsnapshot.id

        snapshot_model_updates = []
        for i, snapshot in enumerate(snapshots):
            snapshot_update = {'id': snapshot['id']}
            try:
                snap_name = snap_name_base + "-" + six.text_type(i)
                snap_info = client.getSnapshotByName(snap_name)
                client.deleteSnapshot(snap_info['id'])
                snapshot_update['status'] = 'deleted'
            except hpeexceptions.HTTPServerError as ex:
                in_use_msg = ('cannot be deleted because it is a clone '
                              'point')
                if in_use_msg in ex.get_description():
                    LOG.error(_LE("The snapshot cannot be deleted because "
                                  "it is a clone point."))
                snapshot_update['status'] = 'error'
            except Exception as ex:
                LOG.error(_LE("There was an error deleting snapshot %(id)s: "
                              "%(error)."),
                          {'id': snapshot['id'],
                           'error': six.text_type(ex)})
                snapshot_update['status'] = 'error'
            snapshot_model_updates.append(snapshot_update)

        self._logout(client)

        model_update = {'status': cgsnapshot.status}

        return model_update, snapshot_model_updates

    def create_snapshot(self, snapshot):
        """Creates a snapshot."""
        client = self._login()
        try:
            volume_info = client.getVolumeByName(snapshot['volume_name'])

            option = {'inheritAccess': True}
            client.createSnapshot(snapshot['name'],
                                  volume_info['id'],
                                  option)
        except Exception as ex:
            raise exception.VolumeBackendAPIException(ex)
        finally:
            self._logout(client)

    def delete_snapshot(self, snapshot):
        """Deletes a snapshot."""
        client = self._login()
        try:
            snap_info = client.getSnapshotByName(snapshot['name'])
            client.deleteSnapshot(snap_info['id'])
        except hpeexceptions.HTTPNotFound:
            LOG.error(_LE("Snapshot did not exist. It will not be deleted"))
        except hpeexceptions.HTTPServerError as ex:
            in_use_msg = 'cannot be deleted because it is a clone point'
            if in_use_msg in ex.get_description():
                raise exception.SnapshotIsBusy(snapshot_name=snapshot['name'])

            raise exception.VolumeBackendAPIException(ex)

        except Exception as ex:
            raise exception.VolumeBackendAPIException(ex)
        finally:
            self._logout(client)

    def get_volume_stats(self, refresh=False):
        """Gets volume stats."""
        client = self._login()
        try:
            if refresh:
                self._update_backend_status(client)

            return self.device_stats
        finally:
            self._logout(client)

    def _update_backend_status(self, client):
        data = {}
        backend_name = self.configuration.safe_get('volume_backend_name')
        data['driver_version'] = self.VERSION
        data['volume_backend_name'] = backend_name or self.__class__.__name__
        data['reserved_percentage'] = (
            self.configuration.safe_get('reserved_percentage'))
        data['storage_protocol'] = 'iSCSI'
        data['vendor_name'] = 'Hewlett Packard Enterprise'
        data['location_info'] = (self.DRIVER_LOCATION % {
            'cluster': self.configuration.hpelefthand_clustername,
            'vip': self.cluster_vip})
        data['thin_provisioning_support'] = True
        data['thick_provisioning_support'] = True
        data['max_over_subscription_ratio'] = (
            self.configuration.safe_get('max_over_subscription_ratio'))

        cluster_info = client.getCluster(self.cluster_id)

        total_capacity = cluster_info['spaceTotal']
        free_capacity = cluster_info['spaceAvailable']

        # convert to GB
        data['total_capacity_gb'] = int(total_capacity) / units.Gi
        data['free_capacity_gb'] = int(free_capacity) / units.Gi

        # Collect some stats
        capacity_utilization = (
            (float(total_capacity - free_capacity) /
             float(total_capacity)) * 100)
        # Don't have a better way to get the total number volumes
        # so try to limit the size of data for now. Once new lefthand API is
        # available, replace this call.
        total_volumes = 0
        provisioned_size = 0
        volumes = client.getVolumes(
            cluster=self.configuration.hpelefthand_clustername,
            fields=['members[id]', 'members[clusterName]', 'members[size]'])
        if volumes:
            total_volumes = volumes['total']
            provisioned_size = sum(
                members['size'] for members in volumes['members'])
        data['provisioned_capacity_gb'] = int(provisioned_size) / units.Gi
        data['capacity_utilization'] = capacity_utilization
        data['total_volumes'] = total_volumes
        data['filter_function'] = self.get_filter_function()
        data['goodness_function'] = self.get_goodness_function()
        data['consistencygroup_support'] = True

        self.device_stats = data

    def initialize_connection(self, volume, connector):
        """Assigns the volume to a server.

        Assign any created volume to a compute node/host so that it can be
        used from that host. HPE VSA requires a volume to be assigned
        to a server.
        """
        client = self._login()
        try:
            server_info = self._create_server(connector, client)
            volume_info = client.getVolumeByName(volume['name'])

            access_already_enabled = False
            if volume_info['iscsiSessions'] is not None:
                # Extract the server id for each session to check if the
                # new server already has access permissions enabled.
                for session in volume_info['iscsiSessions']:
                    server_id = int(session['server']['uri'].split('/')[3])
                    if server_id == server_info['id']:
                        access_already_enabled = True
                        break

            if not access_already_enabled:
                client.addServerAccess(
                    volume_info['id'],
                    server_info['id'])

            iscsi_properties = self._get_iscsi_properties(volume)

            if ('chapAuthenticationRequired' in server_info and
                    server_info['chapAuthenticationRequired']):
                iscsi_properties['auth_method'] = 'CHAP'
                iscsi_properties['auth_username'] = connector['initiator']
                iscsi_properties['auth_password'] = (
                    server_info['chapTargetSecret'])

            return {'driver_volume_type': 'iscsi', 'data': iscsi_properties}
        except Exception as ex:
            raise exception.VolumeBackendAPIException(ex)
        finally:
            self._logout(client)

    def terminate_connection(self, volume, connector, **kwargs):
        """Unassign the volume from the host."""
        client = self._login()
        try:
            volume_info = client.getVolumeByName(volume['name'])
            server_info = client.getServerByName(connector['host'])
            volume_list = client.findServerVolumes(server_info['name'])

            removeServer = True
            for entry in volume_list:
                if entry['id'] != volume_info['id']:
                    removeServer = False
                    break

            client.removeServerAccess(
                volume_info['id'],
                server_info['id'])

            if removeServer:
                client.deleteServer(server_info['id'])
        except Exception as ex:
            raise exception.VolumeBackendAPIException(ex)
        finally:
            self._logout(client)

    def create_volume_from_snapshot(self, volume, snapshot):
        """Creates a volume from a snapshot."""
        client = self._login()
        try:
            snap_info = client.getSnapshotByName(snapshot['name'])
            volume_info = client.cloneSnapshot(
                volume['name'],
                snap_info['id'])
            return self._update_provider(volume_info)
        except Exception as ex:
            raise exception.VolumeBackendAPIException(ex)
        finally:
            self._logout(client)

    def create_cloned_volume(self, volume, src_vref):
        client = self._login()
        try:
            volume_info = client.getVolumeByName(src_vref['name'])
            clone_info = client.cloneVolume(volume['name'], volume_info['id'])
            return self._update_provider(clone_info)
        except Exception as ex:
            raise exception.VolumeBackendAPIException(ex)
        finally:
            self._logout(client)

    def _get_volume_extra_specs(self, volume):
        """Get extra specs from a volume."""
        extra_specs = {}
        type_id = volume.get('volume_type_id', None)
        if type_id is not None:
            ctxt = context.get_admin_context()
            volume_type = volume_types.get_volume_type(ctxt, type_id)
            extra_specs = volume_type.get('extra_specs')
        return extra_specs

    def _get_lh_extra_specs(self, extra_specs, valid_keys):
        """Get LeftHand extra_specs (valid_keys only)."""
        extra_specs_of_interest = {}
        for key, value in extra_specs.items():
            if key in valid_keys:
                prefix = key.split(":")
                if prefix[0] == "hplh":
                    LOG.warning(_LW("The 'hplh' prefix is deprecated. Use "
                                    "'hpelh' instead."))
                extra_specs_of_interest[key] = value
        return extra_specs_of_interest

    def _map_extra_specs(self, extra_specs):
        """Map the extra spec key/values to LeftHand key/values."""
        client_options = {}
        for key, value in extra_specs.items():
            # map extra spec key to lh client option key
            client_key = extra_specs_key_map[key]
            # map extra spect value to lh client option value
            try:
                value_map = extra_specs_value_map[client_key]
                # an invalid value will throw KeyError
                client_value = value_map[value]
                client_options[client_key] = client_value
            except KeyError:
                LOG.error(_LE("'%(value)s' is an invalid value "
                              "for extra spec '%(key)s'"),
                          {'value': value, 'key': key})
        return client_options

    def _update_provider(self, volume_info):
        # TODO(justinsb): Is this always 1? Does it matter?
        cluster_interface = '1'
        iscsi_portal = self.cluster_vip + ":3260," + cluster_interface

        return {'provider_location': (
            "%s %s %s" % (iscsi_portal, volume_info['iscsiIqn'], 0))}

    def _create_server(self, connector, client):
        server_info = None
        chap_enabled = self.configuration.hpelefthand_iscsi_chap_enabled
        try:
            server_info = client.getServerByName(connector['host'])
            chap_secret = server_info['chapTargetSecret']
            if not chap_enabled and chap_secret:
                LOG.warning(_LW('CHAP secret exists for host %s but CHAP is '
                                'disabled'), connector['host'])
            if chap_enabled and chap_secret is None:
                LOG.warning(_LW('CHAP is enabled, but server secret not '
                                'configured on server %s'), connector['host'])
            return server_info
        except hpeexceptions.HTTPNotFound:
            # server does not exist, so create one
            pass

        optional = None
        if chap_enabled:
            chap_secret = utils.generate_password()
            optional = {'chapName': connector['initiator'],
                        'chapTargetSecret': chap_secret,
                        'chapAuthenticationRequired': True
                        }

        server_info = client.createServer(connector['host'],
                                          connector['initiator'],
                                          optional)
        return server_info

    def create_export(self, context, volume, connector):
        pass

    def ensure_export(self, context, volume):
        pass

    def remove_export(self, context, volume):
        pass

    def retype(self, ctxt, volume, new_type, diff, host):
        """Convert the volume to be of the new type.

        Returns a boolean indicating whether the retype occurred.

        :param ctxt: Context
        :param volume: A dictionary describing the volume to retype
        :param new_type: A dictionary describing the volume type to convert to
        :param diff: A dictionary with the difference between the two types
        :param host: A dictionary describing the host, where
                     host['host'] is its name, and host['capabilities'] is a
                     dictionary of its reported capabilities.
        """
        LOG.debug('enter: retype: id=%(id)s, new_type=%(new_type)s,'
                  'diff=%(diff)s, host=%(host)s', {'id': volume['id'],
                                                   'new_type': new_type,
                                                   'diff': diff,
                                                   'host': host})
        client = self._login()
        try:
            volume_info = client.getVolumeByName(volume['name'])

            # pick out the LH extra specs
            new_extra_specs = dict(new_type).get('extra_specs')
            lh_extra_specs = self._get_lh_extra_specs(
                new_extra_specs,
                extra_specs_key_map.keys())

            LOG.debug('LH specs=%(specs)s', {'specs': lh_extra_specs})

            # only set the ones that have changed
            changed_extra_specs = {}
            for key, value in lh_extra_specs.items():
                (old, new) = diff['extra_specs'][key]
                if old != new:
                    changed_extra_specs[key] = value

            # map extra specs to LeftHand options
            options = self._map_extra_specs(changed_extra_specs)
            if len(options) > 0:
                client.modifyVolume(volume_info['id'], options)
            return True
        except hpeexceptions.HTTPNotFound:
            raise exception.VolumeNotFound(volume_id=volume['id'])
        except Exception as ex:
            LOG.warning(_LW("%s"), ex)
        finally:
            self._logout(client)

        return False

    def migrate_volume(self, ctxt, volume, host):
        """Migrate the volume to the specified host.

        Backend assisted volume migration will occur if and only if;

        1. Same LeftHand backend
        2. Volume cannot be attached
        3. Volumes with snapshots cannot be migrated
        4. Source and Destination clusters must be in the same management group

        Volume re-type is not supported.

        Returns a boolean indicating whether the migration occurred, as well as
        model_update.

        :param ctxt: Context
        :param volume: A dictionary describing the volume to migrate
        :param host: A dictionary describing the host to migrate to, where
                     host['host'] is its name, and host['capabilities'] is a
                     dictionary of its reported capabilities.
        """
        LOG.debug('enter: migrate_volume: id=%(id)s, host=%(host)s, '
                  'cluster=%(cluster)s', {
                      'id': volume['id'],
                      'host': host,
                      'cluster': self.configuration.hpelefthand_clustername})

        false_ret = (False, None)
        if 'location_info' not in host['capabilities']:
            return false_ret

        host_location = host['capabilities']['location_info']
        (driver, cluster, vip) = host_location.split(' ')
        client = self._login()
        try:
            # get the cluster info, if it exists and compare
            cluster_info = client.getClusterByName(cluster)
            LOG.debug('Cluster info: %s', cluster_info)
            virtual_ips = cluster_info['virtualIPAddresses']

            if driver != self.__class__.__name__:
                LOG.info(_LI("Cannot provide backend assisted migration for "
                             "volume: %s because volume is from a different "
                             "backend."), volume['name'])
                return false_ret
            if vip != virtual_ips[0]['ipV4Address']:
                LOG.info(_LI("Cannot provide backend assisted migration for "
                             "volume: %s because cluster exists in different "
                             "management group."), volume['name'])
                return false_ret

        except hpeexceptions.HTTPNotFound:
            LOG.info(_LI("Cannot provide backend assisted migration for "
                         "volume: %s because cluster exists in different "
                         "management group."), volume['name'])
            return false_ret
        finally:
            self._logout(client)

        client = self._login()
        try:
            volume_info = client.getVolumeByName(volume['name'])
            LOG.debug('Volume info: %s', volume_info)

            # can't migrate if server is attached
            if volume_info['iscsiSessions'] is not None:
                LOG.info(_LI("Cannot provide backend assisted migration "
                             "for volume: %s because the volume has been "
                             "exported."), volume['name'])
                return false_ret

            # can't migrate if volume has snapshots
            snap_info = client.getVolume(
                volume_info['id'],
                'fields=snapshots,snapshots[resource[members[name]]]')
            LOG.debug('Snapshot info: %s', snap_info)
            if snap_info['snapshots']['resource'] is not None:
                LOG.info(_LI("Cannot provide backend assisted migration "
                             "for volume: %s because the volume has "
                             "snapshots."), volume['name'])
                return false_ret

            options = {'clusterName': cluster}
            client.modifyVolume(volume_info['id'], options)
        except hpeexceptions.HTTPNotFound:
            LOG.info(_LI("Cannot provide backend assisted migration for "
                         "volume: %s because volume does not exist in this "
                         "management group."), volume['name'])
            return false_ret
        except hpeexceptions.HTTPServerError as ex:
            LOG.error(_LE("Exception: %s"), ex)
            return false_ret
        finally:
            self._logout(client)

        return (True, None)

    def update_migrated_volume(self, context, volume, new_volume,
                               original_volume_status):
        """Rename the new (temp) volume to it's original name.


        This method tries to rename the new volume to it's original
        name after the migration has completed.

        """
        LOG.debug("Update volume name for %(id)s.", {'id': new_volume['id']})
        name_id = None
        provider_location = None
        if original_volume_status == 'available':
            # volume isn't attached and can be updated
            original_name = CONF.volume_name_template % volume['id']
            current_name = CONF.volume_name_template % new_volume['id']
            client = self._login()
            try:
                volume_info = client.getVolumeByName(current_name)
                volumeMods = {'name': original_name}
                client.modifyVolume(volume_info['id'], volumeMods)
                LOG.info(_LI("Volume name changed from %(tmp)s to %(orig)s."),
                         {'tmp': current_name, 'orig': original_name})
            except Exception as e:
                LOG.error(_LE("Changing the volume name from %(tmp)s to "
                              "%(orig)s failed because %(reason)s."),
                          {'tmp': current_name, 'orig': original_name,
                           'reason': e})
                name_id = new_volume['_name_id'] or new_volume['id']
                provider_location = new_volume['provider_location']
            finally:
                self._logout(client)
        else:
            # the backend can't change the name.
            name_id = new_volume['_name_id'] or new_volume['id']
            provider_location = new_volume['provider_location']

        return {'_name_id': name_id, 'provider_location': provider_location}

    def manage_existing(self, volume, existing_ref):
        """Manage an existing LeftHand volume.

        existing_ref is a dictionary of the form:
        {'source-name': <name of the virtual volume>}
        """
        # Check API Version
        self._check_api_version()

        target_vol_name = self._get_existing_volume_ref_name(existing_ref)

        # Check for the existence of the virtual volume.
        client = self._login()
        try:
            volume_info = client.getVolumeByName(target_vol_name)
        except hpeexceptions.HTTPNotFound:
            err = (_("Virtual volume '%s' doesn't exist on array.") %
                   target_vol_name)
            LOG.error(err)
            raise exception.InvalidInput(reason=err)
        finally:
            self._logout(client)

        # Generate the new volume information based on the new ID.
        new_vol_name = 'volume-' + volume['id']

        volume_type = None
        if volume['volume_type_id']:
            try:
                volume_type = self._get_volume_type(volume['volume_type_id'])
            except Exception:
                reason = (_("Volume type ID '%s' is invalid.") %
                          volume['volume_type_id'])
                raise exception.ManageExistingVolumeTypeMismatch(reason=reason)

        new_vals = {"name": new_vol_name}

        client = self._login()
        try:
            # Update the existing volume with the new name.
            client.modifyVolume(volume_info['id'], new_vals)
        finally:
            self._logout(client)

        LOG.info(_LI("Virtual volume '%(ref)s' renamed to '%(new)s'."),
                 {'ref': existing_ref['source-name'], 'new': new_vol_name})

        display_name = None
        if volume['display_name']:
            display_name = volume['display_name']

        if volume_type:
            LOG.info(_LI("Virtual volume %(disp)s '%(new)s' is "
                         "being retyped."),
                     {'disp': display_name, 'new': new_vol_name})

            try:
                self.retype(None,
                            volume,
                            volume_type,
                            volume_type['extra_specs'],
                            volume['host'])
                LOG.info(_LI("Virtual volume %(disp)s successfully retyped to "
                             "%(new_type)s."),
                         {'disp': display_name,
                          'new_type': volume_type.get('name')})
            except Exception:
                with excutils.save_and_reraise_exception():
                    LOG.warning(_LW("Failed to manage virtual volume %(disp)s "
                                    "due to error during retype."),
                                {'disp': display_name})
                    # Try to undo the rename and clear the new comment.
                    client = self._login()
                    try:
                        client.modifyVolume(
                            volume_info['id'],
                            {'name': target_vol_name})
                    finally:
                        self._logout(client)

        updates = {'display_name': display_name}

        LOG.info(_LI("Virtual volume %(disp)s '%(new)s' is "
                     "now being managed."),
                 {'disp': display_name, 'new': new_vol_name})

        # Return display name to update the name displayed in the GUI and
        # any model updates from retype.
        return updates

    def manage_existing_get_size(self, volume, existing_ref):
        """Return size of volume to be managed by manage_existing.

        existing_ref is a dictionary of the form:
        {'source-name': <name of the virtual volume>}
        """
        # Check API version.
        self._check_api_version()

        target_vol_name = self._get_existing_volume_ref_name(existing_ref)

        # Make sure the reference is not in use.
        if re.match('volume-*|snapshot-*', target_vol_name):
            reason = _("Reference must be the volume name of an unmanaged "
                       "virtual volume.")
            raise exception.ManageExistingInvalidReference(
                existing_ref=target_vol_name,
                reason=reason)

        # Check for the existence of the virtual volume.
        client = self._login()
        try:
            volume_info = client.getVolumeByName(target_vol_name)
        except hpeexceptions.HTTPNotFound:
            err = (_("Virtual volume '%s' doesn't exist on array.") %
                   target_vol_name)
            LOG.error(err)
            raise exception.InvalidInput(reason=err)
        finally:
            self._logout(client)

        return int(math.ceil(float(volume_info['size']) / units.Gi))

    def unmanage(self, volume):
        """Removes the specified volume from Cinder management."""
        # Check API version.
        self._check_api_version()

        # Rename the volume's name to unm-* format so that it can be
        # easily found later.
        client = self._login()
        try:
            volume_info = client.getVolumeByName(volume['name'])
            new_vol_name = 'unm-' + six.text_type(volume['id'])
            options = {'name': new_vol_name}
            client.modifyVolume(volume_info['id'], options)
        finally:
            self._logout(client)

        LOG.info(_LI("Virtual volume %(disp)s '%(vol)s' is no longer managed. "
                     "Volume renamed to '%(new)s'."),
                 {'disp': volume['display_name'],
                  'vol': volume['name'],
                  'new': new_vol_name})

    def _get_existing_volume_ref_name(self, existing_ref):
        """Returns the volume name of an existing reference.

        Checks if an existing volume reference has a source-name element.
        If source-name is not present an error will be thrown.
        """
        if 'source-name' not in existing_ref:
            reason = _("Reference must contain source-name.")
            raise exception.ManageExistingInvalidReference(
                existing_ref=existing_ref,
                reason=reason)

        return existing_ref['source-name']

    def _check_api_version(self):
        """Checks that the API version is correct."""
        if (self.api_version < MIN_API_VERSION):
            ex_msg = (_('Invalid HPELeftHand API version found: %(found)s. '
                        'Version %(minimum)s or greater required for '
                        'manage/unmanage support.')
                      % {'found': self.api_version,
                         'minimum': MIN_API_VERSION})
            LOG.error(ex_msg)
            raise exception.InvalidInput(reason=ex_msg)

    def _get_volume_type(self, type_id):
        ctxt = context.get_admin_context()
        return volume_types.get_volume_type(ctxt, type_id)
